%%% ------------------------------------------------------------------
%%% Licensed under the Apache License, Version 2.0 (the 'License');
%%%  you may not use this file except in compliance with the License.
%%%  You may obtain a copy of the License at
%%%
%%%      http://www.apache.org/licenses/LICENSE-2.0
%%%
%%% Copyright (c) 2017 qingchuwudi <bypf2009@vip.qq.com>
%%%
%%%  Unless required by applicable law or agreed to in writing, software
%%%  distributed under the License is distributed on an 'AS IS' BASIS,
%%%  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%%%  See the License for the specific language governing permissions and
%%%  limitations under the License.
%%%
%%% @doc
%%% @author  qingchuwudi <'bypf2009@vip.qq.com'>
%%% @copyright 2017 qingchuwudi <bypf2009@vip.qq.com>
%%% @end
%%% created|changed : 2017-09-05 11:52
%%% coding : utf-8
%%% ------------------------------------------------------------------
-module(elcache).
-author("qingchuwudi").

-behaviour(gen_server).
-export([
    start_link/0
]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).

-export([
    add_pool/5,
    delete_pool/1,
    add/2,
    change_function/2
]).

-include("elcache.hrl").

%% ------------------------------------------------------------------
%%    对外api
%% ------------------------------------------------------------------

start_link() ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, {}, []).

%%@doc add cache pool
add_pool(PoolId, ProcessCount, CacheLength, Timeout, Fun) when
            is_atom(PoolId) andalso
            (is_integer(ProcessCount) andalso ProcessCount > 0) andalso
            (is_integer(CacheLength) andalso CacheLength > 0) andalso
            (is_integer(Timeout) andalso Timeout > 100) andalso
            is_function(Fun, 1) ->
    Opts = [PoolId, ProcessCount, CacheLength, Timeout, Fun],
    gen_server:call(?MODULE, {add_pool, Opts}).

delete_pool(PoolId) ->
    gen_server:call(?MODULE, {delete_pool, PoolId}).

%%@doc 缓存新数据
-spec add(PoolId, Data) -> Re when
        PoolId :: atom(),
        Data :: any(),
        Re :: ok.
add(PoolId, Data) when is_atom(PoolId) ->
    Pid = elcache_router:get_elcache(PoolId),
    elcache_service:add(Pid, Data).

%%@doc 改变缓存数据处理函数
-spec change_function(PoolId, Fun) -> Re when
        PoolId :: atom(),
        Fun :: fun(),
        Re :: ok.
change_function(PoolId, Fun) when is_atom(PoolId)
                             andalso is_function(Fun, 1) ->
    gen_server:call(?MODULE, {change_function, PoolId, Fun}).

%% ------------------------------------------------------------------
%%    Inner api
%% ------------------------------------------------------------------

-spec start_pool(SupRef, ProcessCount, CacheLength, Timeout, Fun) -> Re when
        SupRef :: pid(),
        ProcessCount :: non_neg_integer(),
        CacheLength :: non_neg_integer(),
        Timeout :: non_neg_integer(),
        Fun :: fun(),
        Re :: list().
%%@doc 启动服务
start_pool(SupRef, ProcessCount, CacheLength, Timeout, Fun) ->
    ElcacheArgs = #elcache{count   = CacheLength,
                           timeout = Timeout,
                           function= Fun,
                           cache_list  = [],
                           cache_count = 0},
    start_service(ProcessCount, SupRef, [ElcacheArgs], []).

%%@private start elcache services.
start_service(0, _, _, PidList) ->
    PidList;
start_service(ProcessCount, SupRef, ElcacheArgs, PidList) ->
    Bytes = crypto:rand_bytes(4),
    BString = bin2hex(Bytes),
    IdBinary = <<"elcache_", BString/binary >>,
    Id = erlang:binary_to_atom(IdBinary, utf8),
    {ok, Pid} = elcache_service_sup:start_child(SupRef, Id, ElcacheArgs),
    start_service(ProcessCount - 1, SupRef, ElcacheArgs, [Pid | PidList]).

bin2hex(IoData) when is_binary(IoData) ->
    << <<Y>> || <<X:4>> <= IoData, Y <- integer_to_list(X,16)>>.

%% ------------------------------------------------------------------
%%    回调函数
%% ------------------------------------------------------------------

%% 初始化
init({}) ->
    elcache_dynamic:pool_init(#{}),
    {ok, maps:new()}.

%% 同步
handle_call({add_pool, [PoolId, ProcessCount, CacheLength, Timeout, Fun]},
            _From, Pools) ->
    case maps:find(PoolId, Pools) of
        {ok, _} ->
            {reply, exist, Pools};
        error ->
            {ok, PoolPid} = elcache_sup:start_super(PoolId),
            Pids = start_pool(PoolPid, ProcessCount, CacheLength, Timeout, Fun),
            Opts = #{
                id     => PoolId,
                poolpid=> PoolPid,
                count  => ProcessCount,
                length => CacheLength,
                timeout  => Timeout,
                function => Fun,
                pids => Pids
            },
            NewPools = Pools#{PoolId => Opts},
            Res = elcache_dynamic:pool_init(NewPools),
            {reply, Res, NewPools}
    end;

handle_call({change_function, PoolId, Fun}, _From, Pools) ->
    case maps:find(PoolId, Pools) of
        {ok, #{pids := Pids}} ->
            lists:foreach(
                fun(Pid) ->
                    elcache_service:change_function(Pid, Fun)
                end, Pids),
            {reply, ok, Pools};
        error ->
            {reply, undefined, Pools}
    end;

handle_call({delete_pool, PoolId}, _From, Pools) ->
    case maps:find(PoolId, Pools) of
        {ok, #{poolpid := PoolPid, pids := Pids}} ->
            [elcache_service_sup:stop_child(PoolPid, Pid) || Pid <- Pids],
            elcache_sup:stop_super(PoolId),
            NewPools = maps:remove(PoolId, Pools),
            Res = elcache_dynamic:pool_init(NewPools),
            {reply, Res, NewPools};
        error ->
            {reply, undefined, Pools}
    end;

handle_call(_Request, _From, Pools) ->
    {reply, {error, unknown_call}, Pools}.

%% 异步
handle_cast(_Msg, Pools) ->
    {noreply, Pools}.

%% 消息
handle_info(_Info, Pools) ->
    {noreply, Pools}.

%% 终止处理
terminate(_Reason, _Pools) ->
    ok.

%% 热更替换
code_change(_OldVsn, Pools, _Extra) ->
    {ok, Pools}.